﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Threading;
using System.Net;
using System.Resources;
using System.Collections.ObjectModel;

namespace Bonobo.Irc.Client
{
    public class IrcSession : INotifyPropertyChanged, IDisposable
    {
        // FIXME: it's preferable to initialize things inline when possible, 
        //        please also group the static and readonly things together
        
        private IrcConnection _connection;
        private readonly SynchronizationContext _syncContext;

        // FIXME: make readonly, initialize inline
        private IrcRequestFactory _requestFactory = new IrcRequestFactory();

        // FIXME: make readonly, initialize inline
        private IrcConnectionInfo _connectionInfo;

        // FIXME: initialize inline
        private readonly Queue<IrcRequest> _requestQue;

        private static readonly Object EventStateChanged = new Object();
        private readonly EventHandlerList _events = new EventHandlerList();
        
        private IrcSessionState _state;

        // FIXME: remove
        private PropertyChangedEventHandler _propertyChanged;

        private readonly ObservableCollection<IrcConversation> _conversations = new ObservableCollection<IrcConversation>();
        
        // FIXME: make readonly
        private readonly IrcServerConversation _serverConversation;
        private readonly object _lock = new Object();

        public IrcSession()
        {
            _connectionInfo = new IrcConnectionInfo();
            _requestQue = new Queue<IrcRequest>();
            _syncContext = SynchronizationContext.Current;
            ChangeState(IrcSessionState.Closed);
            _serverConversation = new IrcServerConversation(this);
            _conversations.Add(_serverConversation);
        }



        public event EventHandler<IrcStateChangedEventArgs> StateChanged
        {
            add { _events.AddHandler(EventStateChanged, value); }
            remove { _events.RemoveHandler(EventStateChanged, value); }
        }

        public IrcSessionState State
        {
            get { return _state; }
            private set
            {
                if (_state != value)
                {
                    _state = value;
                    OnPropertyChanged("State");
                }
            }
        }

        public IrcConnectionInfo ConnectionInfo { get { return _connectionInfo; } }

        public ObservableCollection<IrcConversation> Conversations { get { return _conversations; } }

        public IrcServerConversation ServerConversation
        {
            get { return _serverConversation; } 
        }

        public void Open()
        {
            if (ConnectionInfo.Address == null || ConnectionInfo.Port <= 0)
            {
                throw new InvalidOperationException(Resources.AddressAndPortException);
            }

            _requestFactory = new IrcRequestFactory(ConnectionInfo.Username);
            _connection = new IrcConnection(new IPEndPoint(ConnectionInfo.Address, ConnectionInfo.Port));
            _connection.StateChanged += OnStateChanged;
            _connection.MessageReceived += OnMessageReceived;
            _connection.MessageSent += OnMessageSent;
            _connection.BeginOpen();
            ChangeState(IrcSessionState.Opening);          
            Register();
        }

        public void Dispose()
        {
            if (_connection != null)
            {
                _connection.Dispose();
            }
        }

        private void Register()
        {
            if (!String.IsNullOrEmpty(ConnectionInfo.ConnectionPassword))
            {
                Send(_requestFactory.SetConnectionPassword(ConnectionInfo.ConnectionPassword));
            }
            Send(_requestFactory.SetNick(ConnectionInfo.Username));
            Send(_requestFactory.SetUser(ConnectionInfo.Username, ConnectionInfo.Realname));

            ChangeState(IrcSessionState.Opened);
        }

        public void Close()
        {
            Close(null);
        }

        public void Close(String message)
        {
            ChangeState(IrcSessionState.Closing);
            Send(_requestFactory.Quit(message));
        }



        private void OnMessageReceived(Object sender, IrcMessageEventArgs e)
        {
            var messages = e.Message.Split(new String[] { "\r", "\n", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var item in messages)
            {
                HandleMessage(item);
            }
        }

        // FIXME: refactor into a separate method
        private void OnMessageSent(Object sender, IrcMessageEventArgs e)
        {            
            lock (_requestQue)
            {
                if (_requestQue.Count > 0)
                {
                    _connection.BeginSend(_requestQue.Dequeue());
                }
            }
            if (State == IrcSessionState.Closing)
            {
                ChangeState(IrcSessionState.Closed);
                _connection.Dispose();
            }
        }

        // FIXME: see OnMessageSent
        private void OnStateChanged(Object sender, EventArgs e)
        {
            if (((IrcConnection)sender).State == IrcConnectionState.Opened)
            {
                lock (_requestQue)
                {
                    if (_requestQue.Count > 0)
                    {
                        _connection.BeginSend(_requestQue.Dequeue());
                    }
                }
            }
            else if (((IrcConnection)sender).State == IrcConnectionState.Faulted)
            {
                ChangeState(IrcSessionState.ServerError, new IrcException(Resources.ConnectionNotEstablished));
            }
        }

        // FIXME: should not rather throw InvalidOperationException when not open?
        private void Send(IrcRequest request)
        {
            if (_connection != null && _connection.State == IrcConnectionState.Opened)
            {
                _connection.BeginSend(request);
            }
            else
            {
                _requestQue.Enqueue(request);
            }
        }

        private void Pong(String deamon)
        {
            Send(_requestFactory.Pong(deamon));
        }

        // FIXME: ugh...
        private void NotifyChangeState(IrcSessionState state, IrcException exception)
        {
            var previousState = _state;
            ChangeState(state, exception);
            ChangeState(previousState);
        }

        // FIXME: dtto
        private void NotifyChangeState(IrcSessionState state, String message)
        {
            var previousState = _state;
            ChangeState(state, message);
            ChangeState(previousState);
        }

        private void ChangeState(IrcSessionState state, IrcException exception)
        {
            lock (_lock)
            {
                State = state;
                IrcStateChangedEventArgs arg;
                arg = new IrcStateChangedEventArgs(exception);
                _events.InvokeEvent(EventStateChanged, this, arg);
            }
        }

        private void ChangeState(IrcSessionState state)
        {
            ChangeState(state, String.Empty);
        }

        // FIXME: refactor into a single method accepting IrcStateChangedEventArgs 
        private void ChangeState(IrcSessionState state, String message)
        {
            lock (_lock)
            {
                State = state;
                IrcStateChangedEventArgs arg;
                arg = new IrcStateChangedEventArgs(message);
                _events.InvokeEvent(EventStateChanged, this, arg);
            }
        }

        // FIXME: all IrcException-bearing events should change the state to Faulted. 
        //        user should begin connecting afresh if anything failed. the class may 
        //        recycle the connection, if possible, but the State should be Faulted.

        //        also consider making some kind of a hashtable mapping response types to exceptions
        //        and getting rid of this ugly long switch

        private void HandleMessage(String message)
        {
            var request = new IrcRequest();
            var response = new IrcResponse();

            if (IrcRequest.TryParse(message, out request))
            {
                if (request.Type.Equals(IrcProtocol.Requests.Ping, StringComparison.Ordinal))
                {
                    Pong(request.Arguments[0]);
                }
            }
            else if (IrcResponse.TryParse(message, out response))
            {
                if (response.Type == IrcResponseType.ErrorNotRegistred)
                {
                    ChangeState(IrcSessionState.Opening, new IrcException(String.Format(Resources.NotRegistred, response.Arguments[0])));
                }
                else if (response.Type == IrcResponseType.ErrorNeedMoreParams)
                {
                    NotifyChangeState(IrcSessionState.Error, new IrcException(String.Format(Resources.NeedMoreParams, response.Arguments[0])));
                }
                else if (response.Type == IrcResponseType.ErrorAlreadyRegistred)
                {
                    NotifyChangeState(IrcSessionState.Error, new IrcException(String.Format(Resources.AlreadyRegistred, response.Arguments[0])));
                }
                else if (response.Type == IrcResponseType.ErrorNoNicknameGiven)
                {
                    ChangeState(IrcSessionState.Opening, new IrcException(String.Format(Resources.NoNickname, response.Arguments[0])));
                }
                else if (response.Type == IrcResponseType.ErrorErrorneusNickname)
                {
                    ChangeState(IrcSessionState.Opening, new IrcException(String.Format(String.Format(Resources.ErrorneusNickname, response.Arguments[0], response.Arguments[1]))));
                }
                else if (response.Type == IrcResponseType.ErrorNicknameInUse)
                {
                    ChangeState(IrcSessionState.Opening, new IrcException(String.Format(Resources.NicknameInUse, response.Arguments[0], response.Arguments[1])));
                }
                else if (response.Type == IrcResponseType.ErrorNicknameCollision)
                {
                    ChangeState(IrcSessionState.Opening, new IrcException(String.Format(Resources.NicknameCollision, response.Arguments[0], response.Arguments[1])));
                }
                else if (response.Type == IrcResponseType.UnknownResponse)
                {
                    var msg = response.Arguments.Last();
                    NotifyChangeState(IrcSessionState.Notification, msg);
                }

                AddMessageToConversation(response);

            }
            else
            {
                NotifyChangeState(IrcSessionState.Error, new IrcException(String.Format(Resources.UnknownServerResponse, message)));
                AddMessageToConversation(message);
            }

        }

        private void AddMessageToConversation(IrcResponse response)
        {
            // TODO: Handle message categorizing to proper conversation
             _serverConversation.Messages.Add(new IrcServerMessage()
            {
                TimeStamp = DateTime.Now,
                Message = response.ToString(),
                IsLoginError = IrcResponse.IsLoginError(response.Type),
                Type = IrcMessageType.ServerResponse
            });
        }

        private void AddMessageToConversation(IrcRequest request)
        {
            // TODO: Handle message categorizing to proper conversation
            throw new NotImplementedException();
        }

        private void AddMessageToConversation(String unknownCommand)
        {
            _serverConversation.Messages.Add(new IrcServerMessage()
            {
                TimeStamp = DateTime.Now,
                Message = unknownCommand,
                Type = IrcMessageType.UnknownMessage
            });
        }

        private void OnPropertyChanged(String propertyName)
        {
            var handler = _propertyChanged;

            if (handler != null)
            {
                var args = new PropertyChangedEventArgs(propertyName);

                SynchronizationContext.Current.Post(() => handler(this, args));
            }
        }

        // FIXME: use _events
        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
        {
            add { _propertyChanged += value; }
            remove { _propertyChanged -= value; }
        }
    }
}
